package com.googlecode.continuity.plugin.syntaxhighlight.client;

import com.google.gwt.user.client.*;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.googlecode.continuity.plugin.gwtext.client.DOMExtension;
import com.googlecode.continuity.plugin.gwtext.client.FlexTableWithListeners;

/**
 * SelectableLinesHighlightArea
 * <p/>
 * TODO: make more efficient with native html implementation instead of tons of panels
 *
 * @author Peter Schmitt <aragos@aragos.de>
 */
public class SelectableLinesHighlightArea extends Composite implements SourcesSelectedLinesEvents {
// ------------------------------ FIELDS ------------------------------

  private static final String HIGHLIGHT_STYLE_DEFAULT = "selectedLine";
  protected int numberColumn = 0;
  protected int codeColumn = 1;
  protected FlexTableWithListeners lines;
  protected int startLine;
  protected String highlightStyle;
  protected VerticalPanel area;
  private int startSelectedLine;
  private int endSelectedLine;
  private EventPreview catchMouse;
  private boolean selecting;
  private SelectedLinesListenerCollection selectedLinesListenerCollection;

// --------------------------- CONSTRUCTORS ---------------------------

  public SelectableLinesHighlightArea() {
    // set defaults
    selecting = false;
    startLine = 1;
    highlightStyle = HIGHLIGHT_STYLE_DEFAULT;

    // initialize panel
    // todo: move to addListener
    selectedLinesListenerCollection = new SelectedLinesListenerCollection();
    area = new VerticalPanel();
    lines = new FlexTableWithListeners();

    area.addStyleName("dp-highlightArea");
    lines.addStyleName("lines");
    area.add(lines);
    initWidget(area);

    catchMouse = new EventPreview() {
      public boolean onEventPreview(Event event) {
        int type = DOM.eventGetType(event);
        if (type == Event.ONMOUSEDOWN) {
          endSelect();
        } else if (type == Event.ONMOUSEUP) {
          doSelect(event);
        } else if (type == Event.ONMOUSEMOVE) {
          int x = DOM.eventGetClientX(event) + Window.getScrollLeft();
          int y = DOM.eventGetClientY(event) + Window.getScrollTop();
          progressSelect(x, y);
        }
        return true;
      }
    };
  }

  private void endSelect() {
    removeSelectHighlight();
    DOM.removeEventPreview(catchMouse);
    selecting = false;
  }

  private void removeSelectHighlight() {
    for (int i = 0; i < lines.getRowCount(); i++) {
      lines.getRowFormatter().removeStyleName(i, highlightStyle);
    }
  }

  private void doSelect(Event event) {
    endSelect();
    int firstLine = startSelectedLine < endSelectedLine ? startSelectedLine : endSelectedLine;
    int lastLine = startSelectedLine > endSelectedLine ? startSelectedLine : endSelectedLine;
    selectedLinesListenerCollection.fireLinesSelected(event, firstLine, lastLine);
  }

  private void progressSelect(int x, int y) {
    int newEnd;
    if (y < lines.getAbsoluteTop()) {
      newEnd = startLine;
    } else if (y > lines.getAbsoluteTop() + lines.getOffsetHeight()) {
      newEnd = row2line(lines.getRowCount());
    } else {
      // TODO: assumes that all lines have the same height
      int lineHeight = getLineHeight();
      newEnd = row2line((int) Math.floor((y - lines.getAbsoluteTop()) / lineHeight));
    }
    if (newEnd != this.endSelectedLine) {
      highlightLines(this.endSelectedLine, newEnd);
      this.endSelectedLine = newEnd;
    }
  }

  protected int getLineHeight() {
    return lines.getWidget(0, codeColumn).getOffsetHeight();
  }

  protected int row2line(int row) {
    return row + startLine;
  }

  private void highlightLines(int oldEnd, int newEnd) {
    int firstOldLine = startSelectedLine < oldEnd ? startSelectedLine : oldEnd;
    int lastOldLine = startSelectedLine > oldEnd ? startSelectedLine : oldEnd;

    int firstNewLine = startSelectedLine < newEnd ? startSelectedLine : newEnd;
    int lastNewLine = startSelectedLine > newEnd ? startSelectedLine : newEnd;

    removeLinesStyle(firstOldLine, firstNewLine, highlightStyle);
    addLinesStyle(firstNewLine, lastNewLine + 1, highlightStyle);
    removeLinesStyle(lastNewLine + 1, lastOldLine + 1, highlightStyle);
  }

  /**
   * Adds given style to lines numbered first (incusive) - last (exclusive).
   *
   * @param first first line to be styled
   * @param last  first line not to be styled
   * @param style style name to be added
   */
  protected void addLinesStyle(int first, int last, String style) {
    for (int i = first; i < last; i++) {
      lines.getRowFormatter().addStyleName(line2row(i), style);
    }
  }

  protected int line2row(int line) {
    return line - startLine;
  }

  /**
   * Removes given style from lines numbered first (incusive) - last (exclusive).
   *
   * @param first first line to be unstyled
   * @param last  first line not to be unstyled
   * @param style style name to be removed
   */
  protected void removeLinesStyle(int first, int last, String style) {
    for (int i = first; i < last; i++) {
      lines.getRowFormatter().removeStyleName(line2row(i), style);
    }
  }

// --------------------- GETTER / SETTER METHODS ---------------------

  public int getStartLine() {
    return startLine;
  }

  public void setStartLine(int startLine) {
    this.startLine = startLine;
  }

// ------------------------ INTERFACE METHODS ------------------------

// --------------------- Interface SourcesSelectedLinesEvents ---------------------

  public void addSelectedLinesListener(SelectedLinesListener listener) {
    selectedLinesListenerCollection.add(listener);
  }

  public void removeSelectedLinesListener(SelectedLinesListener listener) {
    selectedLinesListenerCollection.remove(listener);
  }

// -------------------------- OTHER METHODS --------------------------

  public void addLines(String[] lines) {
    for (int i = 0; i < lines.length; i++) {
      addLine(lines[i]);
    }
  }

  public int addLine(String lineCode) {
    final int rowCount = lines.getRowCount();

    // add line gutter
    Label gutterNumber = new Label("" + row2line(rowCount));
    lines.setWidget(rowCount, numberColumn, gutterNumber);
    lines.getFlexCellFormatter().addStyleName(rowCount, numberColumn, "gutter");
    lines.getFlexCellFormatter().addStyleName(rowCount, numberColumn, "number");
    createGutterListener(rowCount, numberColumn);

    // add code
    HTML code = new HTML(lineCode);
    lines.getFlexCellFormatter().addStyleName(rowCount, codeColumn, "code");
    lines.setWidget(rowCount, codeColumn, code);

    // set row style
    lines.getRowFormatter().addStyleName(rowCount, "line");
    lines.getRowFormatter().addStyleName(rowCount, "line" + (rowCount % 2));
    return rowCount;
  }

  protected void createGutterListener(final int rowCount, final int column) {
    final Element gutterCell = lines.getFlexCellFormatter().getElement(rowCount, column);
    lines.setCellListener(gutterCell, new EventListener() {
      public void onBrowserEvent(Event event) {
        switch (DOM.eventGetType(event)) {
          case Event.ONMOUSEDOWN:
            // prevent text selection
            DOM.eventPreventDefault(event);
            initializeSelect(row2line(rowCount));
            break;
        }
      }
    });

    // prevent text selection
    DOMExtension.preventTextSelection(lines.getFlexCellFormatter().getElement(rowCount, column), true);
  }

  private void initializeSelect(int lineNumber) {
    if (selecting) {
      endSelect();
    }
    endSelectedLine = startSelectedLine = lineNumber;
    selecting = true;
    lines.getRowFormatter().addStyleName(line2row(lineNumber), highlightStyle);

    DOM.addEventPreview(catchMouse);
  }
}
